import {
    Input, Directive, ViewContainerRef,
    OnInit, TemplateRef, DoCheck,
    IterableDiffers, IterableDiffer
} from '@angular/core';

@Directive({
    selector: '[lazyFor]'
})
export class LazyForDirective implements DoCheck, OnInit {

    lazyForContainer: HTMLElement;

    itemHeight: number;
    itemTagName: string;

    @Input()
    set lazyForOf(list: any[]) {
        this.list = list;

        if (list) {
            this.differ = this.iterableDiffers.find(list).create();

            if (this.initialized) {
                this.update();
            }
        }
    }

    private templateElem: HTMLElement;

    private beforeListElem: HTMLElement;
    private afterListElem: HTMLElement;

    private list: any[] = [];

    private initialized = false;
    private firstUpdate = true;

    private differ: IterableDiffer<any>;

    private lastChangeTriggeredByScroll = false;

    constructor(private vcr: ViewContainerRef,
        private tpl: TemplateRef<any>,
        private iterableDiffers: IterableDiffers) { }

    ngOnInit() {
        this.templateElem = this.vcr.element.nativeElement;

        this.lazyForContainer = this.templateElem.parentElement;

        //Adding an event listener will trigger ngDoCheck whenever the event fires so we don't actually need to call
        //update here.
        this.lazyForContainer.addEventListener('scroll', () => {
            this.lastChangeTriggeredByScroll = true;
        });

        this.initialized = true;
    }

    ngDoCheck() {
        if (this.differ && Array.isArray(this.list)) {

            if (this.lastChangeTriggeredByScroll) {
                this.update();
                this.lastChangeTriggeredByScroll = false;
            } else {
                const changes = this.differ.diff(this.list);

                if (changes !== null) {
                    this.update();
                }
            }
        }
    }

    /**
     * List update
     *
     * @returns {void}
     */
    private update(): void {

        //Can't run the first update unless there is an element in the list
        if (this.list.length === 0) {
            this.vcr.clear();
            if (!this.firstUpdate) {
                this.beforeListElem.style.height = '0';
                this.afterListElem.style.height = '0';
            }
            return;
        }

        if (this.firstUpdate) {
            this.onFirstUpdate();
        }

        const listHeight = this.lazyForContainer.clientHeight;
        const scrollTop = this.lazyForContainer.scrollTop;

        //The height of anything inside the container but above the lazyFor content
        const fixedHeaderHeight =
            (this.beforeListElem.getBoundingClientRect().top - this.beforeListElem.scrollTop) -
            (this.lazyForContainer.getBoundingClientRect().top - this.lazyForContainer.scrollTop);

        //This needs to run after the scrollTop is retrieved.
        this.vcr.clear();

        let listStartI = Math.floor((scrollTop - fixedHeaderHeight) / this.itemHeight);
        listStartI = this.limitToRange(listStartI, 0, this.list.length);

        let listEndI = Math.ceil((scrollTop - fixedHeaderHeight + listHeight) / this.itemHeight);
        listEndI = this.limitToRange(listEndI, -1, this.list.length - 1);

        for (let i = listStartI; i <= listEndI; i++) {
            this.vcr.createEmbeddedView(this.tpl, {
                $implicit: this.list[i],
                index: i
            });
        }

        this.beforeListElem.style.height = `${listStartI * this.itemHeight}px`;
        this.afterListElem.style.height = `${(this.list.length - listEndI - 1) * this.itemHeight}px`;
    }

    /**
     * First update.
     *
     * @returns {void}
     */
    private onFirstUpdate(): void {

        let sampleItemElem: HTMLElement;
        if (this.itemHeight === undefined || this.itemTagName === undefined) {
            this.vcr.createEmbeddedView(this.tpl, {
                $implicit: this.list[0],
                index: 0
            });
            sampleItemElem = <HTMLElement>(this.templateElem.nextSibling || this.templateElem.previousSibling);
            if (this.itemHeight === undefined) {
                this.itemHeight = sampleItemElem?.clientHeight;
            }
            if (this.itemTagName === undefined) {
                this.itemTagName = sampleItemElem?.tagName;
            }
        }

        this.beforeListElem = document.createElement(this.itemTagName);
        this.templateElem.parentElement.insertBefore(this.beforeListElem, this.templateElem);

        this.afterListElem = document.createElement(this.itemTagName);
        this.templateElem.parentElement.insertBefore(this.afterListElem, this.templateElem.nextSibling);

        // If you want to use <li> elements
        if (this.itemTagName.toLowerCase() === 'li') {
            this.beforeListElem.style.listStyleType = 'none';
            this.afterListElem.style.listStyleType = 'none';
        }

        this.firstUpdate = false;
    }

    /**
     * Limit To Range
     *
     * @param {number} num - Element number.
     * @param {number} min - Min element number.
     * @param {number} max - Max element number.
     *
     * @returns {number}
     */
    private limitToRange(num: number, min: number, max: number) {
        return Math.max(
            Math.min(num, max),
            min
        );
    }
}
